package com.cyf.filmDownload.m3u8.core;

import com.cyf.filmDownload.common.threadPool.ThreadPoolConfig;
import com.cyf.filmDownload.common.util.FileUtil;
import com.cyf.filmDownload.common.util.HexUtil;
import com.cyf.filmDownload.common.util.UrlUtil;
import com.cyf.filmDownload.config.AppConfig;
import com.cyf.filmDownload.m3u8.entity.*;
import com.cyf.filmDownload.m3u8.exception.M3u8Exception;
import com.cyf.filmDownload.m3u8.listener.DownloadListener;
import com.cyf.filmDownload.m3u8.task.DownloadTask;
import com.cyf.filmDownload.okhttp.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.IOException;
import java.nio.file.Paths;
import java.security.Security;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author chenyifan
 * @create 2024-05-28 19:30
 */
@Slf4j
public class M3u8DownloadUtil {

    private static DownloadListener downloadListener;

    static {
        downloadListener = new DownloadListener();
        downloadListener.start();
    }

    /**
     * 解决java不支持AES/CBC/PKCS7Padding模式解密
     * 不然会报错：ECB mode cannot use IV
     */
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 根据 m3u8Url，下载到默认路径
     * @param m3u8Url
     */
    public static boolean downloadToDefaultDir(String m3u8Url, String videoName) {
        return download(m3u8Url, Paths.get(AppConfig.videoDownloadDir, videoName + ".mp4").toString());
    }

    public static boolean download(String m3u8Url, String videoPath) {
        String videoName = FilenameUtils.getBaseName(videoPath);
        String tempMainDir = Paths.get(AppConfig.tsTempDir, videoName).toString();

        M3u8ParserResult parserResult = new M3u8ParserResult();
        parserResult.setOriginalM3u8Url(m3u8Url);
        parserResult.setTempMainDir(tempMainDir);
        parserResult.setTsDir(Paths.get(tempMainDir, "TS").toString());
        parserResult.setVideoName(videoName);
        parserResult.setVideoPath(videoPath);

        // 获取真实的 m3u8 链接，以及 m3u8 内容
        getRealM3u8Address(parserResult);
        // 获取加密信息
        getVideoEncInfo(parserResult);
        // 获取视频分片信息
        getVideoTsList(parserResult);
        // 下载视频分片
        downloadTs(parserResult);
        return true;
    }

    public static void downloadTs(M3u8ParserResult parserResult) {
        List<VideoTs> tsList = parserResult.getTsList();
        log.info("{}开始下载，分片数量：{}", parserResult.getVideoName(), tsList.size());
        DownloadProgress downloadProgress = new DownloadProgress(parserResult);
        // 添加下载监听任务
        downloadListener.addDownloadProgress(parserResult.getVideoName(), downloadProgress);
        tsList.forEach(ts -> ThreadPoolConfig.downloadThreadPool.submit(new DownloadTask(ts, parserResult, downloadProgress)));
    }

    /**
     * 根据一级 m3u8Ur, 获取包含 ts 的 m3u8 链接内容
     * @return
     */
    private static void getRealM3u8Address(M3u8ParserResult parserResult) {
        String firstM3u8Url = parserResult.getOriginalM3u8Url();
        firstM3u8Url = firstM3u8Url.replaceAll("https", "http");
        List<String> lines = getM3U8FileContent(firstM3u8Url);
        if (lines.isEmpty()) {
            throw new M3u8Exception("获取 m3u8 文件失败");
        }

        // 写入临时目录，用于 debug
        FileUtil.writeLinesToFile(Paths.get(parserResult.getTempMainDir(), "1.txt").toString(), lines);

        // 包含 ts 的 m3u8 的 url
        String secondM3u8Uri = null;
        for (String line : lines) {
            // 一层 m3u8 链接直接返回
            if (line.contains("#EXT-X-KEY") || line.contains("#EXTINF")) {
                log.info("{} m3u8 链接只有一级", firstM3u8Url);
                parserResult.setRealM3u8Url(firstM3u8Url);
                parserResult.setLines(lines);
                return;
            }

            //如果含有此字段，则说明ts片段链接需要从第二个m3u8链接获取
            if (line.contains(".m3u8")) {
                secondM3u8Uri = line;
                break;
            }
        }

        if (secondM3u8Uri == null) {
            throw new M3u8Exception(firstM3u8Url + " 存在二级m3u8链接，但未找到");
        }
        // 尝试拼接二层 m3u8 链接
        for (M3u8UrlStrategy strategy : M3u8UrlStrategy.values()) {
            String secondUrl = strategy.generateUrl(firstM3u8Url, secondM3u8Uri);
            log.info("尝试拼接 m3u8 url: {}", secondUrl);
            List<String> secondLines = getM3U8FileContent(secondUrl);
            if (!secondLines.isEmpty()) {
                log.info("拼接m3u8成功, url: {}", secondUrl);
                FileUtil.writeLinesToFile(Paths.get(parserResult.getTempMainDir(), "2.txt").toString(), secondLines);
                parserResult.setRealM3u8Url(secondUrl);
                parserResult.setLines(secondLines);
                return;
            }
        }
        throw new M3u8Exception("获取二级 m3u8 链接失败, 一级m3u8链接为" + firstM3u8Url);
    }


    /**
     * 获取 ts 下载列表
     * @return
     */
    private static void getVideoTsList(M3u8ParserResult parserResult) {
        // 创建下载 ts 的文件夹
        FileUtil.createDir(parserResult.getTsDir());
        // 获取 ts 列表
        List<VideoTs> tsList = new ArrayList<>();
        int startIndex = parserResult.getExtXKeyIndex(), videoIndex = 1;
        List<String> lines = parserResult.getLines();

        // 获取合适的位数，用于格式化 ts 文件名
        String formatter = "%0" + ((int) Math.log10(lines.size()) + 1)  + "d.ts";
        for (int i = startIndex; i < lines.size(); i++) {
            String line = lines.get(i);
            if (!line.startsWith("#")) { // 找出含有 ts 的内容
                String tsName = String.format(formatter, videoIndex);
                String tsDownloadPath = Paths.get(parserResult.getTsDir(), tsName).toString();
                String tsUrl = getTsUrl(parserResult.getRealM3u8Url(), line);
                tsList.add(new VideoTs(videoIndex++, tsName, tsUrl, tsDownloadPath));
            }
        }

        // 记录每个分片对应的下载地址
        List<String> tsUrlList = tsList.stream().map(ele -> ele.getFileName() + ": " + ele.getUrl()).collect(Collectors.toList());
        FileUtil.writeLinesToFile(Paths.get(parserResult.getTempMainDir(), "ts.txt").toString(), tsUrlList);

        parserResult.setTsList(tsList);
        parserResult.setTsCount(tsList.size());
    }

    private static String getTsUrl(String m3u8Url, String ts) {
        // 链接后缀改为 ts
        ts = ts.substring(0, ts.lastIndexOf(".")) + ".ts";
        if (ts.startsWith("http")) { // https://gs.gszyi.com:999/hls/2/20220930/15118/plist-00001.ts，直接返回
            return ts;
        } else { // 729547e1567000022.ts，添加 m3u8 的目录路径
            return m3u8Url.substring(0, m3u8Url.lastIndexOf("/")) + "/" + ts;
        }
    }

    /**
     * 获取加密信息
     * @return
     */
    private static void getVideoEncInfo(M3u8ParserResult parserResult) {
        List<String> lines = parserResult.getLines();
        String encryptMethod = null;
        String encryptUri = null;
        String iv = null;
        for (int i = 0; i < lines.size(); i++) {
            String line = lines.get(i);
            if (line.contains("EXT-X-KEY")) {
                parserResult.setExtXKeyIndex(i);
                line = line.substring(line.indexOf("METHOD"));
                String[] splits = line.split(",");
                //如果含有此字段，则获取加密算法以及获取密钥的链接
                for (String s : splits) {
                    s = s.trim();
                    if (s.startsWith("METHOD=")) {
                        encryptMethod = s.substring("METHOD=".length()).trim().replaceAll("\"", "");
                        parserResult.setEncryptMethod(encryptMethod);
                    } else if (s.startsWith("URI=")) {
                        encryptUri = s.substring("URI=".length()).trim().replaceAll("\"", "");
                        parserResult.setEncryptUri(encryptUri);
                    } else if (s.startsWith("IV=")) {
                        iv = s.substring("IV=".length()).trim().replaceAll("\"", "");
                        parserResult.setIv(iv);
                    }
                }
            }
        }

        if (parserResult.getEncryptMethod() == null || parserResult.getEncryptUri() == null || parserResult.getIv() == null) {
            log.info("{} 对应的视频未加密", parserResult.getOriginalM3u8Url());
            parserResult.setEncrypt(false);
            return;
        }

        parserResult.setEncrypt(true);
        log.info("加密方法：{}, 密钥URI：{}, 初始化向量：{}", parserResult.getEncryptMethod(), parserResult.getEncryptUri(), parserResult.getIv());

        // 获取密钥 key
        String keyUrl;
        String m3u8Url = parserResult.getRealM3u8Url();
        if (UrlUtil.isUrl(encryptUri)) {
            keyUrl = encryptUri;
        } else {
            keyUrl = parserResult.getRealM3u8Url().substring(0, m3u8Url.lastIndexOf("/")) + "/" + encryptUri;
        }
        byte[] key;
        try(Response response = HttpUtil.get(keyUrl)) {
            if (!response.isSuccessful()) {
                throw new M3u8Exception("获取加密 key 失败, keyUrl: " + keyUrl);
            }

            try {
                key = response.body().bytes();
            } catch (IOException e) {
                throw new M3u8Exception("获取加密 key 失败, keyUrl: " + keyUrl);
            }
        }

        log.info("{} 对应的加密key地址：{}", m3u8Url, keyUrl);
        parserResult.setKeyUrl(keyUrl);
        parserResult.setKeyByte(key);

        // 解析 iv
        byte[] ivByte = null;
        if (iv != null) {
            if (iv.startsWith("0x")) { // 16 进制字符串转为 byte数组
                ivByte = HexUtil.hexTobytes(iv.substring(2));
            } else {
                ivByte = iv.getBytes();
            }
        }
        if (ivByte == null || ivByte.length != 16) {
            ivByte = new byte[16];
            Arrays.fill(ivByte, (byte) 0);
        }
        parserResult.setIvByte(ivByte);
    }

    /**
     * 获取 m3u8 文件内容
     * @param m3u8Url
     * @return
     */
    private static List<String> getM3U8FileContent(String m3u8Url) {
        try(Response response = HttpUtil.get(m3u8Url)) {
            if (!response.isSuccessful()) {
                return Collections.emptyList();
            }
            ResponseBody responseBody = response.body();
            if (responseBody == null) {
                return Collections.emptyList();
            }
            String body = responseBody.string();
            return Arrays.stream(StringUtils.split(body, "\n")).collect(Collectors.toList());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}